ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / feature / connections / src / commonMain / kotlin / org / meshtastic / feature / connections / ScannerViewModel.kt
Displaying Raw β’ Download
feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 36.05 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.connections
Tff7b72import T7ee787androidx.lifecycle.ViewModel
Tff7b72import T7ee787androidx.lifecycle.viewModelScope
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787co.touchlab.kermit.Severity
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.SharingStarted
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.asStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.combine
Tff7b72import T7ee787kotlinx.coroutines.flow.distinctUntilChanged
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.flatMapLatest
Tff7b72import T7ee787kotlinx.coroutines.flow.flowOf
Tff7b72import T7ee787kotlinx.coroutines.flow.flowOn
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.map
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.flow.stateIn
Tff7b72import T7ee787kotlinx.coroutines.flow.update
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanStartException
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanStartFailureReason
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanner
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants
Tff7b72import T7ee787org.meshtastic.core.common.util.safeCatchingAll
Tff7b72import T7ee787org.meshtastic.core.datastore.FirmwareRecoveryDataSource
Tff7b72import T7ee787org.meshtastic.core.datastore.RecentAddressesDataSource
Tff7b72import T7ee787org.meshtastic.core.datastore.model.PendingFirmwareRecovery
Tff7b72import T7ee787org.meshtastic.core.datastore.model.RecentAddress
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DeviceType
Tff7b72import T7ee787org.meshtastic.core.model.util.anonymize
Tff7b72import T7ee787org.meshtastic.core.network.repository.NetworkRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioController
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioPrefs
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceRepository
Tff7b72import T7ee787org.meshtastic.core.repository.UiPrefs
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_disabled
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_location_services_disabled
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_missing_permission
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_start_failed
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_too_frequent
Tff7b72import T7ee787org.meshtastic.core.resources.getPluralStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.local_network_permission_denied_hint
Tff7b72import T7ee787org.meshtastic.core.ui.viewmodel.safeLaunch
Tff7b72import T7ee787org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DeviceListEntry
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DiscoveredDevices
Tff7b72import T7ee787org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
Tff7b72import T7ee787kotlin.time.Duration
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
Tff7b72internal Tff7b72val Te6edf3BLE_SCAN_START_FAILURE_RETRY_COOLDOWN Tff7b72= T79c0ff1T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK Tff7b72=
Ta5d6ff"Ta5d6ffBluetooth scan couldn't start. Try again, or toggle Bluetooth if the problem continues.Ta5d6ff"
T8b949e// English fallback for local_network_permission_denied_hint when resource lookup is unavailable. Internal so tests
T8b949e// can assert the exact surfaced text.
Tff7b72internal Tff7b72const Tff7b72val Te6edf3LOCAL_NETWORK_PERMISSION_DENIED_HINT_FALLBACK Tff7b72=
Ta5d6ff"Ta5d6ffLocal network access is turned off for Meshtastic. If this radio is on your local network, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffthe connection will fail until you allow local network access in system settings.Ta5d6ff"
T8b949e/**
* How long to block scan restarts after a scan-start failure.
*
* A cooldown only helps where retrying too soon is itself the problem β Android's scan-start quota, or a registration
* failure that needs time to settle. [BleScanStartFailureReason.BluetoothDisabled] and
* [BleScanStartFailureReason.LocationServicesDisabled] instead clear the moment the user flips a system toggle, so
* holding the scan button dead for 15s after they have fixed it would just look broken.
*/
Tff7b72private Tff7b72fun Td2a8ffeffectiveBleScanRetryCooldownTb4b4b4(Te6edf3reasonTb4b4b4: Te6edf3BleScanStartFailureReasonTb4b4b4, Te6edf3retryAfterTb4b4b4: Te6edf3Duration?Tb4b4b4)Tb4b4b4: Te6edf3Duration Tff7b72=
Tff7b72when Tb4b4b4(Te6edf3reasonTb4b4b4) Tb4b4b4{
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3BluetoothDisabledTb4b4b4,
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3LocationServicesDisabledTb4b4b4,
Tff7b72-Tff7b72> Te6edf3retryAfter Tff7b72?: Te6edf3DurationTb4b4b4.Te6edf3ZERO
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailedTb4b4b4,
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3MissingScanPermissionTb4b4b4,
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ScanningTooFrequentlyTb4b4b4,
Tff7b72-Tff7b72>
Te6edf3maxOfTb4b4b4(Te6edf3BLE_SCAN_START_FAILURE_RETRY_COOLDOWNTb4b4b4, Te6edf3retryAfter Tff7b72?: Te6edf3DurationTb4b4b4.Te6edf3ZEROTb4b4b4)
Tb4b4b4}
T8b949e/**
* Last-resort English copy used only when compose-resources cannot resolve the translated string.
*
* Kept in step with the `bluetooth_scan_*` resources so the degraded path still names the actual problem instead of
* falling back to generic "scan couldn't start" advice for every reason.
*/
Tff7b72private Tff7b72fun Td2a8ffuntranslatedScanStartFailureMessageTb4b4b4(Te6edf3reasonTb4b4b4: Te6edf3BleScanStartFailureReasonTb4b4b4, Te6edf3retryCooldownSecondsTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Tffa657String Tff7b72=
Tff7b72when Tb4b4b4(Te6edf3reasonTb4b4b4) Tb4b4b4{
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ScanningTooFrequently Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72val Te6edf3unit Tff7b72= Tff7b72if Tb4b4b4(Te6edf3retryCooldownSeconds Tff7b72=Tff7b72= T79c0ff1LTb4b4b4) Ta5d6ff"Ta5d6ffsecondTa5d6ff" Tff7b72else Ta5d6ff"Ta5d6ffsecondsTa5d6ff"
Ta5d6ff"Ta5d6ffBluetooth scan limit reached. Try again in Tffd700$Te6edf3retryCooldownSecondsTa5d6ff Tffd700$Te6edf3unitTa5d6ff.Ta5d6ff"
Tb4b4b4}
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3BluetoothDisabled Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffBluetooth is off. Turn it on to scan for nearby devices.Ta5d6ff"
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3LocationServicesDisabled Tff7b72-Tff7b72>
Ta5d6ff"Ta5d6ffLocation services are off. Turn them on to scan for nearby devices.Ta5d6ff"
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailedTb4b4b4,
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3MissingScanPermissionTb4b4b4,
Tff7b72-Tff7b72> Te6edf3BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK
Tb4b4b4}
Tff7b72private Tff7b72fun Te6edf3DurationTb4b4b4.Td2a8ffroundedUpWholeSecondsTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3completeSeconds Tff7b72= Te6edf3inWholeSeconds
Tff7b72return Te6edf3completeSeconds Tff7b72+ Tff7b72if Tb4b4b4(Tff7b72this Tff7b72> Te6edf3completeSecondsTb4b4b4.Te6edf3secondsTb4b4b4) T79c0ff1 Tff7b72else T79c0ff0
Tb4b4b4}
T8b949e/**
* Platform-neutral ViewModel that drives the Connections screen: device discovery (BLE/USB/TCP), scan state, current
* selection, and connection-progress chatter.
*
* Subclassed per-platform (see `AndroidScannerViewModel`, `JvmScannerViewModel`) to plug in platform-specific bonding /
* permission flows.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongParameterListTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)
Tff7b72open Tff7b72class T56d364ScannerViewModelTb4b4b4(
Tff7b72protected Tff7b72val Te6edf3serviceRepositoryTb4b4b4: Te6edf3ServiceRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioControllerTb4b4b4: Te6edf3RadioControllerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceServiceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioPrefsTb4b4b4: Te6edf3RadioPrefsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3recentAddressesDataSourceTb4b4b4: Te6edf3RecentAddressesDataSourceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3getDiscoveredDevicesUseCaseTb4b4b4: Te6edf3GetDiscoveredDevicesUseCaseTb4b4b4,
Tff7b72private Tff7b72val Te6edf3networkRepositoryTb4b4b4: Te6edf3NetworkRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tff7b72private Tff7b72val Te6edf3uiPrefsTb4b4b4: Te6edf3UiPrefsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3firmwareRecoveryDataSourceTb4b4b4: Te6edf3FirmwareRecoveryDataSourceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3bleScannerTb4b4b4: Te6edf3BleScanner? Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3ViewModelTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// ββ Mock / demo transport βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/**
* Whether the Demo Mode entries belong in the device list. Observed rather than sampled once: in a release build
* the gate opens mid-session, when the user performs the hidden-features gesture in Settings.
*/
Tff7b72val Te6edf3showMockTransportTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3mockTransportEnabled
Tff7b72private Tff7b72val Te6edf3_showReplayTransport Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
T8b949e/**
* Whether the replay demo entry should be offered alongside [showMockTransport]. The capture asset is a locally
* generated artifact that no clean-checkout build carries, and without it replay silently degrades to the plain
* mock β so the entry stays hidden rather than advertising behaviour it cannot deliver.
*/
Tff7b72val Te6edf3showReplayTransportTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3_showReplayTransportTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)
T8b949e// ββ Connection-progress chatter (surfaced as the bottom status pill) ββββββββββββββββββββββ
Tff7b72private Tff7b72val Te6edf3_connectionProgressText Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4)
T8b949e/**
* Transient, fine-grained status text emitted during connect/bonding (e.g. "Bondingβ¦", "Requesting configβ¦").
* Nullable because `serviceRepository.connectionProgress` does not emit during steady-state.
*
* Persistent "Not connected / Connecting / Connected" copy is derived separately in
* `ConnectionsViewModel.connectionStatus` so the UI can choose `progress ?: status`.
*/
Tff7b72val Te6edf3connectionProgressTextTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3_connectionProgressTextTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)
T8b949e// ββ BLE scanning ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tff7b72private Tff7b72val Te6edf3_isBleScanning Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
T8b949e/** Whether a BLE scan is currently active. */
Tff7b72val Te6edf3isBleScanningTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3_isBleScanningTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)
T8b949e/** User preference that controls whether BLE scanning auto-starts when the Connections screen opens. */
Tff7b72val Te6edf3bleAutoScanTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3uiPrefsTb4b4b4.Te6edf3bleAutoScan
Tff7b72private Tff7b72val Te6edf3scannedBleDevices Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3MapTff7b72<Tffa657StringTb4b4b4, Te6edf3BleDeviceTff7b72>Tff7b72>Tb4b4b4(Te6edf3emptyMapTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3discoveryOrder Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ListTff7b72<Tffa657StringTff7b72>Tff7b72>Tb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72var Te6edf3scanJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3scanStartFailureCooldownJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
Tff7b72private Tff7b72val Te6edf3scanStartFailureCooldownActive Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72private Tff7b72var Te6edf3scanStartFailureCooldownGeneration Tff7b72= T79c0ff0
T8b949e// Generation counter that owns the `_isBleScanning` flag's cleanup. The scan coroutine's `finally` block may run
T8b949e// asynchronously on the IO dispatcher after a stop+restart has already kicked off a new scan; without this guard
T8b949e// the old job's finally would reset the flag on the new scan's state. Bumped on each start and each stop so that
T8b949e// only the current generation's finally may clear the flag.
Tff7b72private Tff7b72val Te6edf3scanGeneration Tff7b72= Te6edf3MutableStateFlowTb4b4b4(T79c0ff0Tb4b4b4)
T8b949e// ββ Network scanning (NSD gating) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tff7b72private Tff7b72val Te6edf3_isNetworkScanning Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
T8b949e/** Whether an NSD network scan is currently active. */
Tff7b72val Te6edf3isNetworkScanningTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3_isNetworkScanningTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)
T8b949e/** User preference that controls whether NSD network scanning auto-starts when the Connections screen opens. */
Tff7b72val Te6edf3networkAutoScanTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3uiPrefsTb4b4b4.Te6edf3networkAutoScan
T8b949e/**
* Resolved NSD services flow, gated by [_isNetworkScanning]. When scanning is inactive, emits `emptyList()` so
* `NsdManager.discoverServices()` is never triggered. Android 15+ shows a system consent dialog the first time
* `resolvedList` is subscribed, so the gate ensures NSD only runs when the user explicitly requests it.
*/
Tff7b72private Tff7b72val Te6edf3gatedResolvedList Tff7b72=
Te6edf3_isNetworkScanningTb4b4b4.Te6edf3flatMapLatest Tb4b4b4{ Te6edf3scanning Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3scanningTb4b4b4) Te6edf3networkRepositoryTb4b4b4.Te6edf3resolvedList Tff7b72else Te6edf3flowOfTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3discoveredDevicesFlowTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3DiscoveredDevicesTff7b72> Tff7b72=
Te6edf3combineTb4b4b4(Te6edf3showMockTransportTb4b4b4, Te6edf3showReplayTransportTb4b4b4, Tff7b72::Te6edf3PairTb4b4b4)
Tb4b4b4.Te6edf3flatMapLatest Tb4b4b4{ Tb4b4b4(Te6edf3showMockTb4b4b4, Te6edf3showReplayTb4b4b4) Tff7b72-Tff7b72>
Te6edf3getDiscoveredDevicesUseCaseTb4b4b4.Te6edf3invokeTb4b4b4(Te6edf3showMockTb4b4b4, Te6edf3showReplayTb4b4b4, Te6edf3gatedResolvedListTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3initialValue Tff7b72= Te6edf3DiscoveredDevicesTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72init Tb4b4b4{
T8b949e// Sampled once, unlike [showMockTransport]: whether the replay capture ships is fixed when the build is
T8b949e// assembled, so there is nothing for it to react to.
Te6edf3_showReplayTransportTb4b4b4.Te6edf3value Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3isReplayTransportAvailable
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionProgressTb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3_connectionProgressTextTb4b4b4.Te6edf3value Tff7b72= Tffa657it Tb4b4b4}Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3viewModelScopeTb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionState
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3state Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3state Tff7b72is Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Te6edf3stopAllScansTb4b4b4(Tb4b4b4)
Te6edf3clearRecoveryIfConnectedDeviceReturnedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3viewModelScopeTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffScannerViewModel createdTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffonClearedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72superTb4b4b4.Te6edf3onClearedTb4b4b4(Tb4b4b4)
Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffScannerViewModel clearedTa5d6ff" Tb4b4b4}
Tb4b4b4}
T8b949e// ββ Device lists for UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/**
* BLE devices for the UI β restricted to those currently visible via an active scan.
*
* Previously bonded / system-paired peripherals that aren't advertising right now are intentionally excluded so the
* list reflects what's actually nearby. The currently-selected device is the one exception: it's always kept so the
* active connection stays visible (a connected radio stops advertising and would otherwise drop out).
*
* Sorted for stability to prevent "shifting" as advertisements arrive: bonded devices appear first (sorted by
* name), followed by unbonded scanned devices in the order they were first discovered. RSSI updates are reflected
* on the cards but do not trigger a re-sort.
*/
Tff7b72val Te6edf3bleDevicesForUiTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tff7b72> Tff7b72=
Te6edf3combineTb4b4b4(
Te6edf3discoveredDevicesFlowTb4b4b4,
Te6edf3scannedBleDevicesTb4b4b4,
Te6edf3discoveryOrderTb4b4b4,
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4,
Tb4b4b4) Tb4b4b4{ Te6edf3discoveredTb4b4b4, Te6edf3scannedMapTb4b4b4, Te6edf3orderTb4b4b4, Te6edf3selectedAddress Tff7b72-Tff7b72>
T8b949e// Surface a bonded device only when it's currently visible via scan (advertising) or it's the selected
T8b949e// device β this hides stale system-bonded peripherals that aren't nearby.
Tff7b72val Te6edf3bonded Tff7b72=
Te6edf3discoveredTb4b4b4.Te6edf3bleDevicesTb4b4b4.Te6edf3filterIsInstanceTff7b72<Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3filter Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3address Tff7b72in Te6edf3scannedMap Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedAddress
Tb4b4b4}
Tff7b72val Te6edf3bondedAddresses Tff7b72= Te6edf3bondedTb4b4b4.Te6edf3mapToTb4b4b4(Te6edf3mutableSetOfTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3address Tb4b4b4}
T8b949e// Scanned-but-not-bonded devices are explicitly flagged unbonded so the UI routes through
T8b949e// requestBonding() β which on Android triggers createBond() for the pairing dialog before connecting.
T8b949e// Preserves discovery order to prevent items jumping around during the scan burst.
Tff7b72val Te6edf3unbondedScanned Tff7b72=
Te6edf3order
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657it Tff7b72!in Te6edf3bondedAddresses Tb4b4b4}
Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3address Tff7b72-Tff7b72>
Te6edf3scannedMapTff7b72[Te6edf3addressTff7b72]Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Tffa657itTb4b4b4, Te6edf3bonded Tff7b72= Tff7b72falseTb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e// For bonded devices, attach the latest scan RSSI (if we've seen an advertisement this session) so the
T8b949e// UI can show the signal indicator, but keep them sorted by name for stability.
Tff7b72val Te6edf3bondedForUi Tff7b72=
Te6edf3bonded
Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3entry Tff7b72-Tff7b72>
Tff7b72val Te6edf3scanned Tff7b72= Te6edf3scannedMapTff7b72[Te6edf3entryTb4b4b4.Te6edf3addressTff7b72]
Tff7b72if Tb4b4b4(Te6edf3scanned Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3scannedTb4b4b4.Te6edf3rssi Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Te6edf3entryTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3device Tff7b72= Te6edf3scannedTb4b4b4) Tff7b72else Te6edf3entry
Tb4b4b4}
Tb4b4b4.Te6edf3sortedBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3name Tb4b4b4}
Te6edf3bondedForUi Tff7b72+ Te6edf3unbondedScanned
Tb4b4b4}
Tb4b4b4.Te6edf3flowOnTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3defaultTb4b4b4)
Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3initialValue Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3usbDevicesForUiTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tff7b72> Tff7b72=
Te6edf3discoveredDevicesFlowTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3usbDevices Tb4b4b4}Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e/** Discovered (NSD) TCP devices for the Connections device list, gated by the network-scan flag. */
Tff7b72val Te6edf3discoveredTcpDevicesForUiTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tff7b72> Tff7b72=
Te6edf3discoveredDevicesFlowTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3discoveredTcpDevices Tb4b4b4}Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e/** Recently-used TCP addresses for the Connections device list. */
Tff7b72val Te6edf3recentTcpDevicesForUiTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tff7b72> Tff7b72=
Te6edf3discoveredDevicesFlowTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3recentTcpDevices Tb4b4b4}Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e/**
* A firmware update that didn't finish, leaving a device stranded in bootloader mode. Non-null drives the recovery
* banner on the Connections screen, whose tap deep-links into the Firmware Update screen to re-flash the device.
*/
Tff7b72val Te6edf3pendingRecoveryTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3PendingFirmwareRecovery?Tff7b72> Tff7b72=
Te6edf3firmwareRecoveryDataSourceTb4b4b4.Te6edf3pendingTb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3initialValue Tff7b72= Tff7b72nullTb4b4b4)
T8b949e// ββ Current selection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/** The currently-selected device address, or `null` when nothing is selected. */
Tff7b72val Te6edf3selectedAddressFlowTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3currentDeviceAddressFlow
T8b949e/** The persisted device name from the last selection, for use as a UI fallback. */
Tff7b72val Te6edf3persistedDeviceNameTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3radioPrefsTb4b4b4.Te6edf3devName
T8b949e/** Non-null variant of [selectedAddressFlow] that substitutes [NO_DEVICE_SELECTED] for `null`. */
Tff7b72val Te6edf3selectedNotNullFlowTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657StringTff7b72> Tff7b72=
Te6edf3selectedAddressFlow
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657it Tff7b72?: Te6edf3NO_DEVICE_SELECTED Tb4b4b4}
Tb4b4b4.Te6edf3stateInWhileSubscribedTb4b4b4(Te6edf3initialValue Tff7b72= Te6edf3selectedAddressFlowTb4b4b4.Te6edf3value Tff7b72?: Te6edf3NO_DEVICE_SELECTEDTb4b4b4)
T8b949e// ββ Active transport pane ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/** The single transport pane currently rendered by the Connections screen. */
Tff7b72val Te6edf3activeTransportTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3DeviceTypeTff7b72> Tff7b72=
Te6edf3combineTb4b4b4(Te6edf3uiPrefsTb4b4b4.Te6edf3selectedConnectionTransportTb4b4b4, Te6edf3selectedAddressFlowTb4b4b4) Tb4b4b4{ Te6edf3preferredTb4b4b4, Te6edf3selectedAddress Tff7b72-Tff7b72>
Te6edf3resolveActiveTransportTb4b4b4(Te6edf3preferredTb4b4b4, Te6edf3selectedAddressTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3stateInTb4b4b4(
Te6edf3scope Tff7b72= Te6edf3viewModelScopeTb4b4b4,
Te6edf3started Tff7b72= Te6edf3SharingStartedTb4b4b4.Te6edf3EagerlyTb4b4b4,
Te6edf3initialValue Tff7b72=
Te6edf3resolveActiveTransportTb4b4b4(Te6edf3uiPrefsTb4b4b4.Te6edf3selectedConnectionTransportTb4b4b4.Te6edf3valueTb4b4b4, Te6edf3selectedAddressFlowTb4b4b4.Te6edf3valueTb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e/** Selects one Connections transport pane and stops scans that cannot belong to that pane. */
Tff7b72fun Td2a8ffselectTransportTb4b4b4(Te6edf3typeTb4b4b4: Te6edf3DeviceTypeTb4b4b4) Tb4b4b4{
Tff7b72when Tb4b4b4(Te6edf3typeTb4b4b4) Tb4b4b4{
Te6edf3DeviceTypeTb4b4b4.Te6edf3BLE Tff7b72-Tff7b72> Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3DeviceTypeTb4b4b4.Te6edf3TCP Tff7b72-Tff7b72> Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3DeviceTypeTb4b4b4.Te6edf3USB Tff7b72-Tff7b72> Te6edf3stopAllScansTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3activeTransportTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3typeTb4b4b4) Te6edf3uiPrefsTb4b4b4.Te6edf3setSelectedConnectionTransportTb4b4b4(Te6edf3typeTb4b4b4)
Tb4b4b4}
T8b949e// ββ Scan commands ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/**
* Starts BLE scanning. Enforces mutual exclusion (cancels any active network scan first). No-op if already scanning
* or if [bleScanner] is null.
*
* The `finally` that clears [_isBleScanning] is guarded by a generation counter so a stale cancellation from a
* prior scan cannot reset the flag on this new scan's state.
*/
Tff7b72fun Td2a8ffstartBleScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_isBleScanningTb4b4b4.Te6edf3value Tff7b72|Tff7b72| Te6edf3bleScanner Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3scanStartFailureCooldownActiveTb4b4b4.Te6edf3valueTb4b4b4) Tff7b72return
T8b949e// Cancel the other scan first so only one flag is ever true. Both stop methods are idempotent.
Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3_isBleScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Tff7b72val Te6edf3generation Tff7b72= Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72+ T79c0ff1
Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72= Te6edf3generation
Te6edf3scanJob Tff7b72=
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffstartBleScanTa5d6ff"Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleScanner
Tb4b4b4.Te6edf3scanTb4b4b4(Te6edf3timeout Tff7b72= Te6edf3DurationTb4b4b4.Te6edf3INFINITETb4b4b4, Te6edf3serviceUuid Tff7b72= Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3SERVICE_UUIDTb4b4b4)
Tb4b4b4.Te6edf3flowOnTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4)
Tb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3device Tff7b72-Tff7b72>
Te6edf3scannedBleDevicesTb4b4b4.Te6edf3update Tb4b4b4{ Te6edf3current Tff7b72-Tff7b72>
Tff7b72val Te6edf3existing Tff7b72= Te6edf3currentTff7b72[Te6edf3deviceTb4b4b4.Te6edf3addressTff7b72]
T8b949e// Replace if RSSI changed so the UI reflects the latest advertisement. Keep the same
T8b949e// instance otherwise to avoid unnecessary recomposition.
Tff7b72if Tb4b4b4(Te6edf3existing Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3existingTb4b4b4.Te6edf3rssi Tff7b72=Tff7b72= Te6edf3deviceTb4b4b4.Te6edf3rssiTb4b4b4) Tb4b4b4{
Te6edf3current
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3current Tff7b72+ Tb4b4b4(Te6edf3deviceTb4b4b4.Te6edf3address Te6edf3to Te6edf3deviceTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3deviceTb4b4b4.Te6edf3address Tff7b72!in Te6edf3discoveryOrderTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3discoveryOrderTb4b4b4.Te6edf3update Tb4b4b4{ Tffa657it Tff7b72+ Te6edf3deviceTb4b4b4.Te6edf3address Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3exTb4b4b4: Te6edf3BleScanStartExceptionTb4b4b4) Tb4b4b4{
Te6edf3handleBleScanStartFailureTb4b4b4(Te6edf3exTb4b4b4, Te6edf3generationTb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Te6edf3generationTb4b4b4) Tb4b4b4{
Te6edf3_isBleScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3scanJob Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Starts BLE scanning for screen-entry auto-scan only when no device is already selected. Manual scan toggles call
* [startBleScan] directly so users can still discover/switch devices while connected.
*/
Tff7b72fun Td2a8ffstartBleAutoScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3activeTransportTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4) Tff7b72return
Tff7b72val Te6edf3selectedAddress Tff7b72= Te6edf3selectedAddressFlowTb4b4b4.Te6edf3value
Tff7b72if Tb4b4b4(Te6edf3selectedAddress Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3selectedAddress Tff7b72!Tff7b72= Te6edf3NO_DEVICE_SELECTEDTb4b4b4) Tff7b72return
Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Cancels the active BLE scan and resets the scanning flag. Idempotent.
*
* Bumps [scanGeneration] so any in-flight `finally` from the cancelled job cannot reset `_isBleScanning` after a
* subsequent [startBleScan] has flipped it back to `true`.
*/
Tff7b72fun Td2a8ffstopBleScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3scanJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3scanJob Tff7b72= Tff7b72null
Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72= Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72+ T79c0ff1
Te6edf3_isBleScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Tb4b4b4}
T8b949e/**
* Toggles BLE scanning. Persists the auto-scan preference only when the scan actually activates, and clears the
* opposite [networkAutoScan] preference to keep persisted state consistent with the runtime mutual-exclusion
* invariant.
*/
Tff7b72fun Td2a8fftoggleBleScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_isBleScanningTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3startBleScanTb4b4b4(Tb4b4b4)
T8b949e// Only persist enable-intent (and clear the opposite pref) if start actually worked β e.g. not
T8b949e// blocked by a null bleScanner.
Tff7b72if Tb4b4b4(Te6edf3_isBleScanningTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3uiPrefsTb4b4b4.Te6edf3setNetworkAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffhandleBleScanStartFailureTb4b4b4(Te6edf3exceptionTb4b4b4: Te6edf3BleScanStartExceptionTb4b4b4, Te6edf3generationTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3generationTb4b4b4) Tff7b72return
Te6edf3scanGenerationTb4b4b4.Te6edf3value Tff7b72= Te6edf3generation Tff7b72+ T79c0ff1
Te6edf3scanJob Tff7b72= Tff7b72null
Te6edf3_isBleScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3retryCooldown Tff7b72= Te6edf3effectiveBleScanRetryCooldownTb4b4b4(Te6edf3exceptionTb4b4b4.Te6edf3reasonTb4b4b4, Te6edf3exceptionTb4b4b4.Te6edf3retryAfterTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3retryCooldown Tff7b72> Te6edf3DurationTb4b4b4.Te6edf3ZEROTb4b4b4) Te6edf3startBleScanRetryCooldownTb4b4b4(Te6edf3retryCooldownTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3exceptionTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffBLE scan could not start: Tffd700${Te6edf3exceptionTb4b4b4.Te6edf3reasonTb4b4b4.Te6edf3androidCodeTffd700}Ta5d6ff (Tffd700${Te6edf3exceptionTb4b4b4.Te6edf3reasonTb4b4b4.Te6edf3descriptionTffd700}Ta5d6ff)Ta5d6ff"
Tb4b4b4}
Tff7b72val Te6edf3retryCooldownSeconds Tff7b72= Te6edf3retryCooldownTb4b4b4.Te6edf3roundedUpWholeSecondsTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3errorMessage Tff7b72=
Te6edf3safeCatchingAll Tb4b4b4{
Tff7b72when Tb4b4b4(Te6edf3exceptionTb4b4b4.Te6edf3reasonTb4b4b4) Tb4b4b4{
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3MissingScanPermission Tff7b72-Tff7b72>
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_missing_permissionTb4b4b4)
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailed Tff7b72-Tff7b72>
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_start_failedTb4b4b4)
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ScanningTooFrequently Tff7b72-Tff7b72>
Te6edf3getPluralStringSuspendTb4b4b4(
Te6edf3ResTb4b4b4.Te6edf3pluralsTb4b4b4.Te6edf3bluetooth_scan_too_frequentTb4b4b4,
Te6edf3retryCooldownSecondsTb4b4b4.Te6edf3coerceAtMostTb4b4b4(Tffa657IntTb4b4b4.Te6edf3MAX_VALUETb4b4b4.Te6edf3toLongTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3retryCooldownSecondsTb4b4b4,
Tb4b4b4)
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3BluetoothDisabled Tff7b72-Tff7b72> Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_disabledTb4b4b4)
Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3LocationServicesDisabled Tff7b72-Tff7b72>
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_location_services_disabledTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3getOrDefaultTb4b4b4(Te6edf3untranslatedScanStartFailureMessageTb4b4b4(Te6edf3exceptionTb4b4b4.Te6edf3reasonTb4b4b4, Te6edf3retryCooldownSecondsTb4b4b4)Tb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3text Tff7b72= Te6edf3errorMessageTb4b4b4, Te6edf3severity Tff7b72= Te6edf3SeverityTb4b4b4.Te6edf3WarnTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffstartBleScanRetryCooldownTb4b4b4(Te6edf3cooldownTb4b4b4: Te6edf3DurationTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3generation Tff7b72= Tff7b72+Tff7b72+Te6edf3scanStartFailureCooldownGeneration
Te6edf3scanStartFailureCooldownActiveTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3scanStartFailureCooldownJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3scanStartFailureCooldownJob Tff7b72=
Te6edf3viewModelScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3cooldownTb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3scanStartFailureCooldownGeneration Tff7b72=Tff7b72= Te6edf3generationTb4b4b4) Tb4b4b4{
Te6edf3scanStartFailureCooldownActiveTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3scanStartFailureCooldownJob Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Starts NSD network scanning. Enforces the same mutual-exclusion invariant as [startBleScan]; starting Network
* cancels any active BLE scan first.
*/
Tff7b72fun Td2a8ffstartNetworkScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4) Tff7b72return
T8b949e// Cancel the other scan first so only one flag is ever true. Both stop methods are idempotent.
Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3_isNetworkScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Tb4b4b4}
T8b949e/** Starts NSD scanning for screen-entry auto-scan only when the Network pane is active. */
Tff7b72fun Td2a8ffstartNetworkAutoScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3activeTransportTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4) Tff7b72return
Tff7b72val Te6edf3selectedAddress Tff7b72= Te6edf3selectedAddressFlowTb4b4b4.Te6edf3value
Tff7b72if Tb4b4b4(Te6edf3selectedAddress Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3selectedAddress Tff7b72!Tff7b72= Te6edf3NO_DEVICE_SELECTEDTb4b4b4) Tff7b72return
Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/** Cancels the active network scan and resets the scanning flag. Idempotent. */
Tff7b72fun Td2a8ffstopNetworkScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3_isNetworkScanningTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Tb4b4b4}
T8b949e/** Stops both BLE and network scans. Idempotent β safe to call when neither scan is active. */
Tff7b72private Tff7b72fun Td2a8ffstopAllScansTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Toggles network scanning. Persists the auto-scan preference only when the scan actually activates, and clears the
* opposite [bleAutoScan] preference to keep persisted state consistent with the runtime mutual-exclusion invariant.
*/
Tff7b72fun Td2a8fftoggleNetworkScanTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3uiPrefsTb4b4b4.Te6edf3setNetworkAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
T8b949e// Only persist enable-intent (and clear the opposite pref) if start actually worked.
Tff7b72if Tb4b4b4(Te6edf3_isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3uiPrefsTb4b4b4.Te6edf3setNetworkAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Persists the user's intent to auto-scan the network on next screen entry without flipping the active scan flag.
* Used by the Connections screen when it must defer the actual scan start until after the system permission grant
* dialog resolves. When [enabled] is `true`, also clears [bleAutoScan] so persisted state mirrors the runtime
* mutual-exclusion invariant (at most one of [bleAutoScan] / [networkAutoScan] may be true).
*/
Tff7b72fun Td2a8ffpersistNetworkAutoScanIntentTb4b4b4(Te6edf3enabledTb4b4b4: Tffa657BooleanTb4b4b4) Tb4b4b4{
Te6edf3uiPrefsTb4b4b4.Te6edf3setNetworkAutoScanTb4b4b4(Te6edf3enabledTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3enabledTb4b4b4) Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4}
T8b949e// ββ Device selection / disconnect βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
T8b949e/** Asynchronously tells the radio controller to connect to [address]. */
Tff7b72fun Td2a8ffchangeDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffAttempting to change device address to Tffd700${Te6edf3addressTb4b4b4.Te6edf3anonymizeTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffchangeDeviceAddressTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3radioControllerTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Persists [address] in the recent-TCP list under [name]. No-op when [address] does not start with
* [TCP_DEVICE_PREFIX].
*/
Tff7b72fun Td2a8ffaddRecentAddressTb4b4b4(Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4, Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3addressTb4b4b4.Te6edf3startsWithTb4b4b4(Te6edf3TCP_DEVICE_PREFIXTb4b4b4)Tb4b4b4) Tff7b72return
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffaddRecentAddressTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3recentAddressesDataSourceTb4b4b4.Te6edf3addTb4b4b4(Te6edf3RecentAddressTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3nameTb4b4b4)Tb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e/** Removes [address] from the recent-TCP list. */
Tff7b72fun Td2a8ffremoveRecentAddressTb4b4b4(Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffremoveRecentAddressTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3recentAddressesDataSourceTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3addressTb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Connects to a manually-entered TCP address. Wraps the manual-entry flow with the same scan-cancel invariant as
* [onSelected]: stops discovery before connection setup so the manual connect does not race an in-progress
* BLE/network scan for radio resources.
*/
Tff7b72fun Td2a8ffconnectToManualAddressTb4b4b4(Te6edf3fullAddressTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3displayAddress Tff7b72= Te6edf3fullAddressTb4b4b4.Te6edf3removePrefixTb4b4b4(Te6edf3TCP_DEVICE_PREFIXTb4b4b4)
Te6edf3stopAllScansTb4b4b4(Tb4b4b4)
Te6edf3uiPrefsTb4b4b4.Te6edf3setSelectedConnectionTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4)
Te6edf3addRecentAddressTb4b4b4(Te6edf3fullAddressTb4b4b4, Te6edf3displayAddressTb4b4b4)
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3fullAddressTb4b4b4)
Tb4b4b4}
T8b949e/**
* Surfaces the local-network warning for a TCP connect that proceeds without `ACCESS_LOCAL_NETWORK` β either the
* permission is permanently denied (no prompt possible) or an in-context request just resolved as a denial. The
* connect is attempted regardless (the target may be a public host or VPN peer, which the permission does not
* govern); this message names the fix for the case where it IS local and the connect is about to time out.
*/
Tff7b72fun Td2a8ffwarnLocalNetworkPermissionDeniedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffwarnLocalNetworkPermissionDeniedTa5d6ff"Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3message Tff7b72=
Te6edf3safeCatchingAll Tb4b4b4{ Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3local_network_permission_denied_hintTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3getOrDefaultTb4b4b4(Te6edf3LOCAL_NETWORK_PERMISSION_DENIED_HINT_FALLBACKTb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3text Tff7b72= Te6edf3messageTb4b4b4, Te6edf3severity Tff7b72= Te6edf3SeverityTb4b4b4.Te6edf3WarnTb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Called by the UI when a device has been tapped. BLE and USB entries may still need bonding/permission β the
* concrete return value tells the caller whether the connection was initiated immediately.
*
* @return `true` if the connection has been initiated; `false` if bonding/permission is pending.
*/
Tff7b72fun Td2a8ffonSelectedTb4b4b4(Te6edf3entryTb4b4b4: Te6edf3DeviceListEntryTb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
T8b949e// Stop discovery the moment the user picks a device, before any connection setup runs. The connect
T8b949e// attempt (BLE GATT or TCP) contends with an active BLE scan for the same radio resources during the
T8b949e// handshake; cancelling here keeps the lifecycle ordered: scan β stop β connect.
Te6edf3stopAllScansTb4b4b4(Tb4b4b4)
Te6edf3recordSelectedTransportTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Te6edf3radioPrefsTb4b4b4.Te6edf3setDevNameTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3nameTb4b4b4)
Te6edf3addRecentAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4, Te6edf3entryTb4b4b4.Te6edf3nameTb4b4b4)
Tff7b72return Tff7b72when Tb4b4b4(Te6edf3entryTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3Ble Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3entryTb4b4b4.Te6edf3bondedTb4b4b4) Tb4b4b4{
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Tff7b72true
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3requestBondingTb4b4b4(Te6edf3entryTb4b4b4)
Tff7b72false
Tb4b4b4}
Tb4b4b4}
Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3Usb Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3entryTb4b4b4.Te6edf3bondedTb4b4b4) Tb4b4b4{
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Tff7b72true
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3requestPermissionTb4b4b4(Te6edf3entryTb4b4b4)
Tff7b72false
Tb4b4b4}
Tb4b4b4}
Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3Tcp Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffonSelectedTcpTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4) Tb4b4b4}
Tff7b72true
Tb4b4b4}
Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3Mock Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Tff7b72true
Tb4b4b4}
Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3Replay Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Tff7b72true
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Initiates the bonding process and connects to the device upon success.
*
* The default implementation connects directly without explicit bonding, which is correct for Desktop/JVM where the
* OS Bluetooth stack handles pairing during the GATT connection. Android overrides this to call `createBond()`
* first.
*/
Tff7b72protected Tff7b72open Tff7b72fun Td2a8ffrequestBondingTb4b4b4(Te6edf3entryTb4b4b4: Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4) Tb4b4b4{
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4)
Tb4b4b4}
T8b949e/** Platform hook for requesting USB permission before connecting; default is a no-op. */
Tff7b72protected Tff7b72open Tff7b72fun Td2a8ffrequestPermissionTb4b4b4(Te6edf3entryTb4b4b4: Te6edf3DeviceListEntryTb4b4b4.Te6edf3UsbTb4b4b4) Tff7b72= Tffa657Unit
T8b949e/** Clears the persisted device name and tells the radio controller to disconnect. */
Tff7b72fun Td2a8ffdisconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioPrefsTb4b4b4.Te6edf3setDevNameTb4b4b4(Tff7b72nullTb4b4b4)
Te6edf3changeDeviceAddressTb4b4b4(Te6edf3NO_DEVICE_SELECTEDTb4b4b4)
Tb4b4b4}
T8b949e/**
* Manually retire the pending recovery record (user dismissed the banner). Needed for a device whose bootloader
* can't be re-flashed over BLE at all β e.g. a stock nRF Legacy-DFU bootloader that never answers its control point
* β where recovery persistently fails and would otherwise nag forever (it only auto-clears on a successful
* reconnect or re-flash). The record is re-created next time a DFU update is triggered.
*/
Tff7b72fun Td2a8ffdismissRecoveryTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffdismissRecoveryTa5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3firmwareRecoveryDataSourceTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e/**
* If the device that had a pending recovery record has since reconnected normally (e.g. its bootloader timed out
* and booted a valid app, or a prior update actually succeeded), retire the record so the banner doesn't linger.
*/
Tff7b72private Tff7b72fun Td2a8ffclearRecoveryIfConnectedDeviceReturnedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeLaunchTb4b4b4(Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffclearRecoveryTa5d6ff"Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3recovery Tff7b72= Te6edf3firmwareRecoveryDataSourceTb4b4b4.Te6edf3pendingTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@safeLaunch
Tff7b72if Tb4b4b4(Te6edf3recoveryTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3valueTb4b4b4) Tb4b4b4{
Te6edf3firmwareRecoveryDataSourceTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffresolveActiveTransportTb4b4b4(Te6edf3preferredTb4b4b4: Te6edf3DeviceType?Tb4b4b4, Te6edf3selectedAddressTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Te6edf3DeviceType Tff7b72=
Te6edf3preferred Tff7b72?: Te6edf3selectedAddressTff7b72?.Te6edf3letTb4b4b4(Te6edf3DeviceTypeTff7b72::Te6edf3fromAddressTb4b4b4) Tff7b72?: Te6edf3DeviceTypeTb4b4b4.Te6edf3BLE
Tff7b72private Tff7b72fun Td2a8ffrecordSelectedTransportTb4b4b4(Te6edf3fullAddressTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3DeviceTypeTb4b4b4.Te6edf3fromAddressTb4b4b4(Te6edf3fullAddressTb4b4b4)Tff7b72?.Te6edf3letTb4b4b4(Te6edf3uiPrefsTff7b72::Te6edf3setSelectedConnectionTransportTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.24s